package org.projectback.utils.Jwt;

import io.jsonwebtoken.*;

import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.projectback.entity.AccountEntity.CustomUser;
import org.projectback.entity.AccountEntity.dataTransferObject.AccountDTO;
import org.projectback.utils.ResultMessage;
import org.projectback.utils.Utils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.core.GrantedAuthority;

import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;

import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;


@Slf4j
@Component
public class JwtUtils {
    /**
     * @param secretKey    密钥key
     * @param minute       jwt过期时间(分钟)
     * @param id           用户id
     * @param name         用户名或邮箱
     * @param role         用户权限
     * @param refreshToken rt
     * @author K·K
     * 这个方法获取到一个JWT密钥
     */
    public String createJwtAccessToken(String secretKey, Long minute, Long id, String name, List<String> role, String refreshToken) {
        SecretKey key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
        long expMillis = System.currentTimeMillis() + minute * 60 * 1000;
        Date exp = new Date(expMillis);
        return Jwts.builder()
                // 设置签名使用的签名算法和签名使用的秘钥
                .signWith(key, Jwts.SIG.HS256)
                // 如果有私有声明，一定要先设置这个自己创建的私有的声明，这个是给builder的claim赋值，一旦写在标准的声明赋值之后，就是覆盖了那些标准的声明的
                .claims()
                .add("id", id)
                .add("name", name)
                .add("role", role)
                .add("refreshToken", refreshToken)
                .and()
                .expiration(exp)
                .compact();
    }

    /**
     * Token解密
     *
     * @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
     * @param token     加密后的token
     * @return claims
     */
    public Claims praseJwt(String secretKey, String token) throws JwtException {
        SecretKey key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));

        String verifyToken = this.verifyAccessToken(token);

        if (verifyToken == null) return null;
        // 得到DefaultJwtParser
        //此时不需要显示的设置解密方式 因为jjwt会根据 key中包含的加密方式进行解密
        Claims claims;
        JwtParser jwtParser = Jwts.parser()
                // 设置验证签名的秘钥
                .verifyWith(key)
                .build();
        //验证签名并解析
        Jws<Claims> jws = jwtParser.parseSignedClaims(verifyToken);
        claims = jws.getPayload();

        return claims;
    }

    /**
     * 验证AccessToken
     *
     * @param token 加密后的token
     * @return token or null
     */
    private String verifyAccessToken(String token) {
        if (token != null && token.startsWith("Bearer ")) {
            return token.substring(7);   // ← 只把 JWT 本体取出来
        }
        return null;
    }

    /**
     * 创建RefreshToken 用于请求新的AccessToken
     */
    public String getRefreshToken(StringRedisTemplate stringRedisTemplate) {
        String refreshToken = getUUID();
        if (stringRedisTemplate.hasKey("refreshToken:" + refreshToken)) {
            getRefreshToken(stringRedisTemplate);
        }
        return refreshToken;
    }

    private String getUUID() {
        return UUID.randomUUID().toString();
    }

    /**
     * 验证 refreshToken
     *
     * @param refreshToken 用于验证的Token
     * @return true 为服务器认可的凭证 可以重新发布AccessToken
     */
    public boolean verifyRefreshToken(StringRedisTemplate redisTemplate, String refreshToken) {
        return refreshToken != null && redisTemplate.hasKey("refreshToken:" + refreshToken);
    }

    /**
     * 将refreshToken 存储进redis内存中 过期时间为一小时
     *
     * @param template     redis实例
     * @param refreshToken uuid(RefreshToken)
     * @param userDetails  用户信息
     */
    public void addRefreshTokenToRedis(StringRedisTemplate template, String refreshToken, String userDetails) {
        if (!verifyRefreshToken(template, refreshToken)) {
            template.opsForValue().set("refreshToken:" + refreshToken, userDetails, 1, TimeUnit.DAYS);
        } else System.out.println("already has refreshToken :" + refreshToken);
    }

    /**
     * @param stringRedisTemplate redis实例
     * @param utils               工具类实例
     * @param jwtProperties       jwt配置 用于生成新的at令牌
     * @param refreshToken        用户持有的RefreshToken
     * @return 新的AT和RT 若是检测到黑名单中的RT 则强制用户下线
     */
    public String rotateRefreshToken(StringRedisTemplate stringRedisTemplate, Utils utils, JwtProperties jwtProperties, String refreshToken) {
        //检测黑名单中是否存在该refreshToken
        if (stringRedisTemplate.hasKey("BlackList_refreshToken:" + refreshToken)) {
            String newRefreshToken = stringRedisTemplate.opsForValue().get("BlackList_refreshToken:" + refreshToken);
            //销毁黑名单
            stringRedisTemplate.delete("BlackList_refreshToken:" + refreshToken);
            //若存在 直接将新令牌一起销毁
            if (stringRedisTemplate.hasKey("refreshToken:" + newRefreshToken)) {
                stringRedisTemplate.delete("refreshToken:" + newRefreshToken);
                return ResultMessage.failure(400, "你的用户信息可能泄露 请尽快修改密码 重新登入!").toJsonString();
            } else return ResultMessage.failure(410, "你的登入令牌过期 请重新登入!").toJsonString();
        }
        if (verifyRefreshToken(stringRedisTemplate, refreshToken)) {
            //获取用户信息 通过redis 中的 RefreshToken
            String userDetails = stringRedisTemplate.opsForValue().get("refreshToken:" + refreshToken);
            //json to object
            CustomUser customUser = (CustomUser) utils.jsonStringToObject(userDetails, CustomUser.class);
            List<String> userRole = customUser.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList();
            //颁发新的refreshToken 同时将旧的refreshToken 加入黑名单 <key>("BlackList_refreshToken:" + refreshToken),<value>("refreshToken:" + refreshToken)
            String newRefreshToken = UUID.randomUUID().toString();
            stringRedisTemplate.delete("refreshToken:" + refreshToken);
            stringRedisTemplate.opsForValue().set("BlackList_refreshToken:" + refreshToken, newRefreshToken, 5, TimeUnit.MINUTES);
            //将new rt 存入 refresh token
            stringRedisTemplate.opsForValue().set("refreshToken:" + newRefreshToken, utils.objectToJsonString(customUser), 1, TimeUnit.DAYS);
            //返回 at 与 new rt
            String AccessToken = createJwtAccessToken(jwtProperties.getSecretKey(), jwtProperties.getUserTtl(), customUser.getId(), customUser.getUsername(), userRole, newRefreshToken);

            return ResultMessage.<Object>success(206, new AccountDTO(AccessToken, newRefreshToken)).toJsonString();
        }
        return ResultMessage.failure(417, "can't create refresh token!").toJsonString();

    }

}
